Why only one post (and no pagination) on this variation of the loop?

get_posts returns an array of post objects. You cannot loop over that using WP_Query object methods. Even if you could, your loop will use the data in the global variable $wp_query as you did not tell it to do anything different. And showposts is long since deprecated. In other words, your code is broken or flawed in several different ways.

You could use foreach to loop over your array.

$paged = (get_query_var('page')) ? get_query_var('page') : 1;
$posts = get_posts(array(
    'numberposts' => 3,
    'paged' => $paged,
));
// show results
foreach ($posts as $p) {
  echo $p->post_title;
}

You could clobber the global $wp_query object.

$paged = (get_query_var('page')) ? get_query_var('page') : 1;
$wp_query = new WP_Query(array(
    'numberposts' => 3,
    'paged' => $paged,
));
// show results
if ( have_posts() ) {
    while ( have_posts() ) {
        the_post(); 
        the_title();
        // display info about the post
    }
}

Or create a new WP_Query object:

$paged = (get_query_var('page')) ? get_query_var('page') : 1;
$my_query = new WP_Query(array(
    'numberposts' => 3,
    'paged' => $paged,
));
// show results
if ( $my_query->have_posts() ) {
    while ( $my_query->have_posts() ) {
        $my_query->the_post(); 
        the_title();
        // display info about the post
    }
}

Or, maybe, create a filter on pre_get_posts to alter the main query. If you want pagination to work, that is your best bet.

add_action(
  'pre_get_posts',
  function($qry) {
    if ($qry->is_main_query()) {
      $qry->set('posts_per_page',3);
    }
  }
);